package com.yntx.usercenter.authentication.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yntx.usercenter.authentication.converter.UserConverter;
import com.yntx.usercenter.authentication.dto.*;
import com.yntx.usercenter.authentication.mapper.DataShardingMapper;
import com.yntx.usercenter.authentication.mapper.UserMapper;
import com.yntx.usercenter.authentication.po.User;
import com.yntx.usercenter.authentication.service.PasswordBCryptEncoder;
import com.yntx.usercenter.authentication.service.TokenService;
import com.yntx.usercenter.authentication.service.UserService;
import com.yntx.usercenter.authentication.vo.UserVo;
import com.yntx.usercenter.authorization.service.RoleService;
import com.yntx.usercenter.authorization.vo.RoleVO;
import com.yntx.usercenter.common.config.TenantUserContextHolder;
import com.yntx.usercenter.common.config.sharding.enums.ShardingTableCacheEnum;
import com.yntx.usercenter.common.constants.RedisKeyConstant;
import com.yntx.usercenter.common.util.TotpUtils;
import com.yntx.usercenter.common.web.ApiResult;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shardingsphere.infra.hint.HintManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 用户服务impl
 *
 * @author jielihaofeng
 * @date 2023-04-24 11:59
 */
@Slf4j
@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {

    private RedisTemplate<String, String> redisTemplate;

    private UserMapper userMapper;

    private PasswordBCryptEncoder passwordBCryptEncoder;

    private TokenService tokenService;

    private RoleService roleService;

    private DataShardingMapper dataShardingMapper;

    @Override
    public ApiResult<Long> register(UserRegisterDto dto) {
        String tenantId = TenantUserContextHolder.get().getTenantId();
        String systemId = TenantUserContextHolder.get().getSystemId();
        // 校验租户
        if (StringUtils.isEmpty(tenantId)) {
            return ApiResult.ofFail("租户ID不能为空！");
        }
        if (StringUtils.isEmpty(systemId)) {
            return ApiResult.ofFail("系统ID不能为空！");
        }
        Boolean isShareUser = TenantUserContextHolder.get().getIsShareUser();
        if (isShareUser) {
            // 租户下用户共享，不按系统分表
            return insertUserByShareUser(tenantId, systemId, dto);
        } else {
            // 租户下用户不共享，按系统分表--与你同行租户就是这个需求
            return insertUserByNotShareUser(tenantId, systemId, dto);
        }
    }

    /**
     * 插入用户共享用户
     *
     * @param tenantId 承租者id
     * @param systemId 系统标识
     * @param dto      dto
     * @return {@link ApiResult}<{@link Long}>
     */
    @DS("#tenantId")
    private ApiResult<Long> insertUserByShareUser(String tenantId, String systemId, UserRegisterDto dto) {
        log.info("#insertUserShareUser# tenantId:{}, systemId:{}, dto:{}", tenantId, systemId, dto);
        return insertUser(systemId, dto);
    }

    /**
     * 插入用户不分享用户
     *
     * @param tenantId 承租者id
     * @param systemId 系统标识
     * @param dto      dto
     * @return {@link ApiResult}<{@link Long}>
     */
    private ApiResult<Long> insertUserByNotShareUser(String tenantId, String systemId, UserRegisterDto dto) {
        // 新的系统用户表不存在时，自动创建表
        autoCreateTable(ShardingTableCacheEnum.USER);
        HintManager.clear();
        try (HintManager instance = HintManager.getInstance()) {
            // 设置分库分表键
            instance.addDatabaseShardingValue(ShardingTableCacheEnum.USER.logicTableName(), tenantId);
            instance.addTableShardingValue(ShardingTableCacheEnum.USER.logicTableName(), systemId);
            return insertUser(systemId, dto);
        } catch (Exception e) {
            log.error("注册异常:", e);
            return ApiResult.ofFail("注册异常：" + e.getMessage());
        }
    }

    /**
     * 插入用户
     *
     * @param systemId 系统标识
     * @param dto      dto
     * @return {@link ApiResult}<{@link Long}>
     */
    private ApiResult<Long> insertUser(String systemId, UserRegisterDto dto) {
        // 校验用户是否存在
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>()
                .eq(User::getUsername, dto.getUsername())
                .eq(User::getSystemId, systemId);
        User user = userMapper.selectOne(lqw);
        if (user != null) {
            return ApiResult.ofFail("用户名已存在！");
        }
        // 写入数据库
        user = User.builder()
                .username(dto.getUsername())
                .password(passwordBCryptEncoder.encode(dto.getPassword()))
                .otpSecret(TotpUtils.generateSecretKey())
                .systemId(Long.valueOf(systemId))
                .build();
        userMapper.insert(user);
        return ApiResult.ofSuccess(user.getId());
    }

    /**
     * 自动创建表
     *
     * @param shardingTableCacheEnum 分片表缓存枚举
     * @return {@link String}
     */
    private String autoCreateTable(ShardingTableCacheEnum shardingTableCacheEnum) {
        String tenantId = TenantUserContextHolder.get().getTenantId();
        String systemId = TenantUserContextHolder.get().getSystemId();
        Set<String> tableNamesCache = shardingTableCacheEnum.resultTableNamesCache();
        String logicTableName = shardingTableCacheEnum.logicTableName();
        String actualTableName = String.format(shardingTableCacheEnum.actualTableNameFormat(), systemId);

        // 自动创建表
        if (!tableNamesCache.contains(actualTableName)) {
            List<String> table = this.existsTable(tenantId, logicTableName);
            if (CollectionUtils.isEmpty(table) || !table.contains(actualTableName)) {
                this.copyTables(tenantId, logicTableName, actualTableName);
                tableNamesCache.add(actualTableName);
            }
        }
        return logicTableName;
    }

    @DS("#tenantId")
    public List<String> existsTable(String tenantId, String tableName) {
        log.info("#existsTable# existsTable:{}, tableName:{}", tenantId, tableName);
        return dataShardingMapper.existsTable(tableName);
    }

    @DS("#tenantId")
    public void copyTables(String tenantId, String originalTable, String newTable) {
        log.info("#existsTable# existsTable:{}, originalTable:{}, newTable:{}", tenantId, originalTable, newTable);
        dataShardingMapper.copyTables(originalTable, newTable);
    }

    @Override
    public ApiResult<UserLoginResultDto> login(UserLoginDto dto) {
        // todo:校验参数
        String tenantId = TenantUserContextHolder.get().getTenantId();
        // 校验租户
        if (StringUtils.isEmpty(tenantId)) {
            return ApiResult.ofFail("租户ID不能为空！");
        }
        Long systemId = Long.parseLong(StringUtils.defaultIfEmpty(TenantUserContextHolder.get().getSystemId(), dto.getSystemId()));
        HintManager.clear();
        try (HintManager instance = HintManager.getInstance()) {
            // 设置分库分表键
            instance.addDatabaseShardingValue("t_user", tenantId);
            instance.addTableShardingValue("t_user", systemId);
            String username = dto.getUsername();
            // todo:接入多种方式登陆查询用户
            LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>()
                    .eq(User::getUsername, username);
            User user = userMapper.selectOne(lqw);
            if (Objects.isNull(user)) {
                // 无论如何只报错提示用户名密码错误，防止攻击
                return ApiResult.ofFail("用户名密码错误！");
            }
            // 校验密码
            String password = user.getPassword();
            if (passwordBCryptEncoder.match(dto.getPassword(), password)) {
                // 无论如何只报错提示用户名密码错误，防止攻击
                return ApiResult.ofFail("用户名密码错误！");
            }
            // 查询已存在的 token
            String key = String.format(RedisKeyConstant.TOKEN_KEY, tenantId, username);
            String token = redisTemplate.opsForValue().get(key);
            if (StringUtils.isEmpty(token)) {
                // 查询用户权限
                List<RoleVO> roleVOList = roleService.listByUser(user.getId());
                List<String> roleList = CollectionUtils.isEmpty(roleVOList) ? null : roleVOList.stream().map(RoleVO::getCode).collect(Collectors.toList());
                // 生成 token
                JwtPayloadDto payloadDto = tokenService.getDefaultJwtPayloadDto(username, tenantId, systemId, roleList);
                token = tokenService.generateToken(payloadDto);
                // 存储 redis
                redisTemplate.opsForValue().set(key, token, Duration.of(2L, ChronoUnit.HOURS));
            }
            return ApiResult.ofSuccess(UserLoginResultDto.builder().token(token).build());
        } catch (Exception e) {
            log.error("登陆异常:", e);
            return ApiResult.ofFail("登陆异常：" + e.getMessage());
        }
    }

    @Override
    public ApiResult<?> logout(UserLogoutDto userLogoutDto) {
        String tenantId = TenantUserContextHolder.get().getTenantId();
        String username = userLogoutDto.getUsername();
        String key = String.format(RedisKeyConstant.TOKEN_KEY, tenantId, username);
        String token = redisTemplate.opsForValue().get(key);
        if (StringUtils.equals(userLogoutDto.getToken(), token)) {
            redisTemplate.delete(key);
            return ApiResult.ofSuccess();
        }
        return ApiResult.ofSuccess();
    }

    @Override
    public ApiResult<UserVo> userInfo(UserInfoDto userInfoDto) {
        String token = redisTemplate.opsForValue().get("user:token:" + userInfoDto.getUsername());
        if (StringUtils.isEmpty(token) || !StringUtils.equals(userInfoDto.getToken(), token)) {
            return ApiResult.ofFail("token已失效");
        }
        LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>()
                .eq(User::getUsername, userInfoDto.getUsername());
        User user = userMapper.selectOne(lqw);
        UserVo userVo = UserConverter.INSTANCE.user2UserVo(user);
        // 前端调用用户信息时静默续期
        redisTemplate.opsForValue().set("user:token:" + user.getUsername(), token, Duration.of(2L, ChronoUnit.HOURS));
        return ApiResult.ofSuccess(userVo);
    }
}
